const vscode = require('vscode');
const path = require('path');
const fs = require('fs');
const prototype = require('./prototype');
const { modify, applyEdits } = require('jsonc-parser');

const JSON_OPTIONS = {
  formattingOptions: {
    insertSpaces: true,
    tabSize: 2,
    eol: '\n',
  },
};

class PlatoStructTreeProvider {
  constructor(context) {
    context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.refresh()));
    context.subscriptions.push(vscode.commands.registerCommand('plato.newStruct', () => this.newStruct()));
    context.subscriptions.push(vscode.commands.registerCommand('plato.deleteStruct', (x) => this.deleteStruct(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.renameStruct', (x) => this.renameStruct(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.newField', (x) => this.newField(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.deleteField', (x) => this.deleteField(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.renameField', (x) => this.renameField(x)));

    this._onDidChangeTreeData = new vscode.EventEmitter();
    this.onDidChangeTreeData = this._onDidChangeTreeData.event;
    this.refresh();
  }

  refresh() {
    if (this.watcher) {
      this.watcher.dispose();
    }

    if (!vscode.workspace.workspaceFolders) {
      vscode.commands.executeCommand('setContext', 'plato:structInitialized', false);
      return;
    }

    const root = vscode.workspace.workspaceFolders[0].uri.fsPath;
    this.path = path.join(root, 'plato.json');

    this.watcher = vscode.workspace.createFileSystemWatcher('**/*.json', false, false, false);
    this.watcher.onDidChange((e) => e.fsPath === this.path && this.refresh());
    this.watcher.onDidCreate((e) => e.fsPath === this.path && this.refresh());
    this.watcher.onDidDelete((e) => e.fsPath === this.path && this.refresh());

    if (!fs.existsSync(this.path)) {
      vscode.commands.executeCommand('setContext', 'plato:structInitialized', false);
      return;
    }

    try {
      this.text = fs.readFileSync(this.path, 'utf-8');
      const json = JSON.parse(this.text);
      this.structs = json.structs;
      this.maxId = 0;
      if (this.structs.length > 0) {
        this.maxId = this.structs.reduce((pre, cur) => (pre.id > cur.id ? pre : cur)).id;
      }

      vscode.commands.executeCommand('setContext', 'plato:structInitialized', true);
      this._onDidChangeTreeData.fire();
    } catch (err) {
      vscode.window.showErrorMessage(err);
      vscode.commands.executeCommand('setContext', 'plato:structInitialized', false);
      return;
    }
  }

  getTreeItem(element) {
    return element;
  }

  getChildren(element) {
    if (element) {
      if (element instanceof StructItem) {
        return Promise.resolve(element.target.fields.map((x) => new FieldItem(x, element)));
      } else {
        return Promise.resolve([]);
      }
    } else {
      return Promise.resolve(this.structs.map((x) => new StructItem(x)));
    }
  }

  updateDocument(path, value) {
    const edits = modify(this.text, path, value, JSON_OPTIONS);
    fs.writeFileSync(this.path, applyEdits(this.text, edits));
  }

  newStruct() {
    vscode.window.showInputBox({ prompt: 'New Struct', placeHolder: 'Title' }).then((val) => {
      if (val) {
        this.updateDocument(['structs', this.structs.length], {
          id: ++this.maxId,
          title: val,
          fields: [],
        });
      }
    });
  }

  deleteStruct(item) {
    const index = this.structs.indexOf(item.target);
    this.updateDocument(['structs', index], undefined);
  }

  renameStruct(item) {
    const index = this.structs.indexOf(item.target);
    vscode.window.showInputBox({ prompt: 'Rename Struct', value: item.target.title }).then((val) => {
      if (val && index >= 0) {
        this.updateDocument(['structs', index], {
          ...item.target,
          title: val,
        });
      }
    });
  }

  newField(item) {
    let title;
    let type;
    let key;
    let value;

    const index = this.structs.indexOf(item.target);
    const newField = () => {
      this.updateDocument(['structs', index, 'fields', item.target.fields.length], {
        title,
        type,
        key,
        value,
      });
    };

    vscode.window
      .showInputBox({ placeHolder: 'Title', prompt: 'New Field: ' })
      .then((val) => {
        if (val) {
          title = val;
          return vscode.window.showQuickPick(prototype.DATA_TYPES.concat(this.structs.map((x) => x.title)), {
            placeHolder: 'Type',
            canPickMany: false,
          });
        }
      })
      .then((val) => {
        if (val) {
          type = val;
          if (prototype.hasKey(type)) {
            return vscode.window.showQuickPick(prototype.SIMPLE_TYPES.concat(this.structs.map((x) => x.title)), {
              placeHolder: 'Key',
              canPickMany: false,
            });
          } else {
            newField();
          }
        }
      })
      .then((val) => {
        if (val) {
          key = val;
          if (prototype.hasValue(type)) {
            return vscode.window.showQuickPick(prototype.SIMPLE_TYPES.concat(this.structs.map((x) => x.title)), {
              placeHolder: 'Value',
              canPickMany: false,
            });
          } else {
            newField();
          }
        }
      })
      .then((val) => {
        if (val) {
          value = val;
          newField();
        }
      });
  }

  deleteField(item) {
    const structIndex = this.structs.indexOf(item.parent.target);
    const fieldIndex = this.structs[structIndex].fields.indexOf(item.target);
    this.updateDocument(['structs', structIndex, 'fields', fieldIndex], undefined);
  }

  renameField(item) {
    const structIndex = this.structs.indexOf(item.parent.target);
    const fieldIndex = this.structs[structIndex].fields.indexOf(item.target);
    vscode.window.showInputBox({ prompt: 'Rename Field:', value: item.target.title }).then((val) => {
      if (val) {
        this.updateDocument(['structs', structIndex, 'fields', fieldIndex], {
          ...item.target,
          title: val,
        });
      }
    });
  }
}

class FieldItem extends vscode.TreeItem {
  constructor(target, parent) {
    super(target.title, vscode.TreeItemCollapsibleState.None);
    this.target = target;
    this.parent = parent;
    this.tooltip = prototype.typeLabel(target.type, target.key, target.value);
    this.iconPath = new vscode.ThemeIcon(prototype.typeIcon(target.type));
    this.contextValue = 'field';
  }
}

class StructItem extends vscode.TreeItem {
  constructor(target) {
    super(target.title, vscode.TreeItemCollapsibleState.Collapsed);
    this.target = target;
    this.iconPath = new vscode.ThemeIcon('symbol-struct');
    this.contextValue = 'struct';
  }
}

module.exports = PlatoStructTreeProvider;
